Для избегания гонок при получении ресурсов применяется механизм блокировок записи, существуют блокировки на таблицы, на страницы и на строки.
Захват блокировки возможен не всегда: ресурс может оказаться уже занятым кем-то другим. Тогда процесс либо встает в очередь ожидания (если механизм блокировки дает такую возможность), либо повторяет попытку захвата блокировки через определенное время. Так или иначе это приводит к тому, что процесс вынужден простаивать в ожидании освобождения ресурса.
По времени использования блокировки можно разделить на длительные и короткие
Долговременные блокировки захватываются на потенциально большое время (обычно до конца транзакции) и чаще всего относятся к таким ресурсам, как таблицы (отношения) и строки. Пользователь, имеет определенный контроль над этим процессом.
Для длительных блокировок характерно большое число режимов, чтобы можно было выполнять как можно больше одновременных действий над данными.
Краткосрочные блокировки захватываются на небольшое время (от нескольких инструкций процессора до долей секунд) и обычно относятся к структурам данных в общей памяти. Такими блокировками PostgreSQL управляет полностью автоматически — об их существовании надо просто знать.
Блокировки объектов располагаются в общей памяти сервера. Их количество ограничено произведением значений двух параметров: max_locks_per_transaction × max_connections.
Каждый из параметров задается при запуске приложения
Также, стоит заметить что это число только определяет размер пулла блокировок. То есть, подключение может превысить свой максимум, но проблемы начнутся, если число блокировок будет суммарно на всех больше чем пулл.
Все блокировки можно посмотреть в представлении pg_locks.
Если ресурс уже заблокирован в несовместимом режиме, транзакция, пытающаяся захватить этот ресурс, ставится в очередь и ожидает освобождения блокировки. Ожидающие транзакции не потребляют ресурсы процессора: соответствующие обслуживающие процессы «засыпают» и пробуждаются операционной системой при освобождении ресурса.
Виды блокировок:
Режимы
Для того чтобы как можно больше одновременных блокировок могло работать с отношениями, было введено целых 8 режимов
Учить это сложно, лучше иметь всегда перед глазами матрицу кто с кем конфликтует, крестик это конфликт
Row не значит что работа со строкой, это просто историческое наследие
режим блокировки | AS | RS | RE | SUE | S | SRE | E | AE | пример SQL-команд |
---|---|---|---|---|---|---|---|---|---|
Access Share | X | SELECT | |||||||
Row Share | X | X | SELECT FOR UPDATE/SHARE | ||||||
Row Exclusive | X | X | X | X | INSERT, UPDATE, DELETE | ||||
Share Update Exclusive | X | X | X | X | X | VACUUM, ALTER TABLE*, СREATE INDEX CONCURRENTLY | |||
Share | X | X | X | X | X | CREATE INDEX | |||
Share Row Exclusive | X | X | X | X | X | X | CREATE TRIGGER, ALTER TABLE* | ||
Exclusive | X | X | X | X | X | X | X | REFRESH MAT. VIEW CONCURRENTLY | |
Access Exclusive | X | X | X | X | X | X | X | X | DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, ALTER TABLE*, REFRESH MAT. VIEW |
Очередь выполнения
Все запросы ждут очереди выполнения с учетом блокировок на отношение и их режима блокировки, никто не обгоняет и действуют по принципу очереди.
Для блокировки строк нет отдельной таблицы с записями кто кого блокирует, для этого применяются некоторые биты самой строки и её поле xmax.
Для блокировок строк существуют 2 исключительных режима
Еще два режима представляют разделяемые (shared) блокировки, которые могут удерживаться несколькими транзакциями.
Команда UPDATE сама выбирает минимальный подходящий режим блокировки; обычно строки блокируются в режиме FOR NO KEY UPDATE.
При удалении или изменении строки в поле xmax текущей актуальной версии записывается номер текущей транзакции. Он показывает, что версия строки удалена данной транзакцией.
Для проверки что изменить версию строки возможно применяется само поле xmax а также дополнительные биты в самой записи.
при блокировки строки, тот кто ждет понимает какая именно транзакция блокирует по полю xmax и начинается ожидание именно транзакции
Вот так выглядит матрица режимов
режим | FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE |
---|---|---|---|---|
FOR KEY SHARE | Х | |||
FOR SHARE | Х | Х | ||
FOR NO KEY UPDATE | Х | Х | Х | |
FOR UPDATE | Х | Х | Х | Х |
У нас может быть, что несколько транзакций блокируют, но поле xmax одно, для этого применяется термин мультитранзакций, это номер который может объединять несколько транзакций, в xmax вписывается номер мультитранзакции в случае если блокираторов несколько.
Обычно команды SQL ожидают освобождения необходимых им ресурсов. Но иногда хочется отказаться от выполнения команды, если блокировку не удалось получить сразу же. Для этого такие команды, как SELECT, LOCK, ALTER, позволяют использовать фразу NOWAIT.
Например:
Блокируем:
=> BEGIN;=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
Просим прочитать
| => SELECT * FROM accounts FOR UPDATE NOWAIT;
Получаем ошибку
| ERROR: could not obtain lock on row in relation "accounts"
Команда немедленно завершается с ошибкой, если ресурс оказался занят. В прикладном коде такую ошибку можно перехватить и обработать.
У команд UPDATE и DELETE фразу NOWAIT указать нельзя, но можно сначала выполнить SELECT FOR UPDATE NOWAIT, а затем — если получилось — обновить или удалить строку.
Есть еще один вариант не ждать — использовать команду SELECT FOR с фразой SKIP LOCKED. Такая команда будет пропускать заблокированные строки, но обрабатывать свободные.l
Как было описано выше, блокировка отпускается только при завершении транзакции, в таком случае возможны ситуации взаимоблокировок.
Например, первая транзакция изменила запись Б и хочет изменить А, вторая транзакция изменила запись А и хочет изменить Б и они друг друга ждут.
Такие ситуации отслеживаются самой PostgreSQL, алгоритм такой
Когда процесс пытается захватить блокировку и не может, он встает в очередь и засыпает, но взводит таймер на значение, указанное в параметре deadlock_timeout (по умолчанию — 1 секунда). Если по истечении deadlock_timeout ожидание продолжается, тогда ожидающий процесс будет разбужен и инициирует проверку на deadlock.
Если проверка (которая состоит в построении графа ожиданий и поиска в нем контуров) не выявила взаимоблокировок, то процесс продолжает спать — теперь уже до победного конца.
параметр lock_timeout, который действует на любой оператор и позволяет избежать неопределенно долгого ожидания: если блокировку не удалось получить за указанное время, оператор завершается с ошибкой lock_not_available
Если же взаимоблокировка выявлена, то одна из транзакций (в большинстве случаев — та, которая инициировала проверку) принудительно обрывается. При этом освобождаются захваченные ей блокировки и остальные транзакции могут продолжать работу.
Такие проверки жрут много ресурсов, нужно строить дерево. Указывают данные проверки на тот факт, что наше приложение сформировано неправильно. Для избегания блокировок достаточно просто принять набор правил о порядке изменения записей, например записи изменяем только по порядку увеличения id, а таблицы только по алфавиту итд.